﻿using System;
using System.Collections.Generic;
using System.Security;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Implements the score model element.
    /// </summary>
    /// <remarks>
    /// A score is a named collection of pages. 
    /// It represents one musical document where the music is aligned into pages
    /// and may be completed by pictures and texts.
    /// </remarks>
    public class ScoreElement : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ScoreElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="ScorePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> is <see langword="null"/>.
        /// </exception>
        internal ScoreElement(ScorePart packagePart)
            : base(packagePart)
        {
            pages = new PathCollection(pagesElement);
            DefaultPageHeight = 29.7;
            DefaultPageWidth = 21.0;
        }

        /// <summary>
        /// Creates a new <see cref="ScoreElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="ScorePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The element that represents this object.
        /// </para>
        /// </param>
        internal ScoreElement(ScorePart packagePart, XmlElement element)
            : base(packagePart, element)
        {
            defaultPageHeightAttribute = element.Attributes["default_page_height"];
            defaultPageWidthAttribute = element.Attributes["default_page_width"];

            metadataElement = element["metadata", PackageVocabulary.PackageNamespaceUri];
            oaiElement = metadataElement["dc", "http://www.openarchives.org/OAI/2.0/oai_dc/"];
            titleElement = oaiElement["title", "http://purl.org/dc/elements/1.1/"];
            subTitleElement = oaiElement.ChildNodes[1] as XmlElement;
            dateElement = oaiElement["date", "http://purl.org/dc/elements/1.1/"];
            descriptionElement = oaiElement["description", "http://purl.org/dc/elements/1.1/"];
            identifierElement = oaiElement["identifier", "http://purl.org/dc/elements/1.1/"];
            languageElement = oaiElement["language", "http://purl.org/dc/elements/1.1/"];
            publisherElement = oaiElement["publisher", "http://purl.org/dc/elements/1.1/"];
            rightsElement = oaiElement["rights", "http://purl.org/dc/elements/1.1/"];
            subjectElement = oaiElement["subject", "http://purl.org/dc/elements/1.1/"];

            pagesElement = element[PackageVocabulary.PageElementName + "s", PackageVocabulary.PackageNamespaceUri];

            pages = new PathCollection(pagesElement);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ScoreElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            metadataElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                "metadata",
                PackageVocabulary.PackageNamespaceUri
                );

            oaiElement = document.CreateElement(
                "oai_dc",
                "dc",
                "http://www.openarchives.org/OAI/2.0/oai_dc/"
                );
            oaiElement.SetAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
            oaiElement.SetAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");

            dateElement = document.CreateElement(
                "dc",
                "date",
                "http://purl.org/dc/elements/1.1/"
                );
            descriptionElement = document.CreateElement(
                "dc",
                "description",
                "http://purl.org/dc/elements/1.1/"
                );
            identifierElement = document.CreateElement(
                "dc",
                "identifier",
                "http://purl.org/dc/elements/1.1/"
                );
            languageElement = document.CreateElement(
                "dc",
                "language",
                "http://purl.org/dc/elements/1.1/"
                );
            publisherElement = document.CreateElement(
                "dc",
                "publisher",
                "http://purl.org/dc/elements/1.1/"
                );
            rightsElement = document.CreateElement(
                "dc",
                "rights",
                "http://purl.org/dc/elements/1.1/"
                );
            subjectElement = document.CreateElement(
                "dc",
                "subject",
                "http://purl.org/dc/elements/1.1/"
                );
            subTitleElement = document.CreateElement(
                "dc",
                "title",
                "http://purl.org/dc/elements/1.1/"
                );
            titleElement = document.CreateElement(
                "dc",
                "title",
                "http://purl.org/dc/elements/1.1/"
                );

            pagesElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.PageElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            metadataElement.AppendChild(oaiElement);
            oaiElement.AppendChild(titleElement);
            oaiElement.AppendChild(subTitleElement);
            oaiElement.AppendChild(subjectElement);
            oaiElement.AppendChild(descriptionElement);
            oaiElement.AppendChild(dateElement);
            oaiElement.AppendChild(identifierElement);
            oaiElement.AppendChild(languageElement);
            oaiElement.AppendChild(publisherElement);
            oaiElement.AppendChild(rightsElement);

            element.AppendChild(metadataElement);
            element.AppendChild(pagesElement);

            defaultPageWidthAttribute = document.CreateAttribute("default_page_width");
            defaultPageHeightAttribute = document.CreateAttribute("default_page_height");

            element.Attributes.Append(defaultPageWidthAttribute);
            element.Attributes.Append(defaultPageHeightAttribute);

            return element;
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Processes an incoming page border request.
        /// </summary>
        /// <param name="request">
        /// <para>
        /// Type: <see cref="Internal.PageBorderRequest"/>
        /// </para>
        /// <para>
        /// The request data.
        /// </para>
        /// </param>
        internal void ProcessBorderRequest(Internal.PageBorderRequest request)
        {
            if (request == null)
                throw new InvalidOperationException("The request data is null!");
            if (request.Request != Internal.BorderRequest.PROCESS_INNER_REQUEST)
                throw new InvalidOperationException("Unrecognized border request!");

            var senderUri = request.Sender.PackagePart.Uri;
            var pageEnumerator = pages.GetEnumerator();

            if (request.RequestDirection == Internal.Direction.Left)
            {
                var prevs = new List<Uri>();
                while (pageEnumerator.MoveNext() && pageEnumerator.Current.URI != senderUri)
                    prevs.Add(pageEnumerator.Current.URI);

                for (int i = prevs.Count - 1; i >= 0; --i)
                {
                    var pagePart = (PagePart)PackagePart.Package.GetPart(prevs[i]);
                    if (pagePart.Page.HandleBorderRequest(request))
                        break;
                }
            }
            else
            {
                while (pageEnumerator.MoveNext() && pageEnumerator.Current.URI != senderUri) ;
                while (pageEnumerator.MoveNext())
                {
                    var pagePart = (PagePart)PackagePart.Package.GetPart(pageEnumerator.Current.URI);
                    if (pagePart.Page.HandleBorderRequest(request))
                        break;
                }
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the date associated with the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// A date associated with an event in the life cycle of the resource.
        /// Typically, Date will be associated with the creation or availability 
        /// of the resource. Recommended best practice for encoding the date 
        /// value is defined in a profile of ISO 8601 [W3CDTF] and follows the YYYY-MM-DD format.
        /// </para>
        /// </value>
        public string Date
        {
            get { return dateElement.InnerText; }
            set
            {
                dateElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Date");
            }
        }

        /// <summary>
        /// Gets or sets the default height of a score page.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The default height (in centimeters) of a score page.
        /// </para>
        /// </value>
        public double DefaultPageHeight
        {
            get { return double.Parse(defaultPageHeightAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                defaultPageHeightAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("DefaultPageHeight");
            }
        }

        /// <summary>
        /// Gets or sets the default width of the score page.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The default width (in centimeters) of a score page.
        /// </para>
        /// </value>
        public double DefaultPageWidth
        {
            get { return double.Parse(defaultPageWidthAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                defaultPageWidthAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("DefaultPageWidth");
            }
        }

        /// <summary>
        /// Gets or sets the description of the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// An account of the content of the resource.
        /// Description may include but is not limited to: an abstract, table of contents, 
        /// reference to a graphical representation of content or a free-text account of the content.
        /// </para>
        /// </value>
        public string Description
        {
            get { return descriptionElement.InnerText; }
            set
            {
                descriptionElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Description");
            }
        }

        /// <summary>
        /// Gets or sets the identifier f the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// An unambiguous reference to the resource within a given context.
        /// Recommended best practice is to identify the resource by means 
        /// of a string or number conforming to a formal identification system. 
        /// Example formal identification systems include the Uniform Resource Identifier (URI) 
        /// (including the Uniform Resource Locator (URL)), the Digital Object Identifier (DOI) 
        /// and the International Standard Book Number (ISBN).
        /// </para>
        /// </value>
        public string Identifier
        {
            get { return identifierElement.InnerText; }
            set
            {
                identifierElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Identifier");
            }
        }

        /// <summary>
        /// Gets or sets the language of the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// A language of the intellectual content of the resource.
        /// Recommended best practice for the values of the Language 
        /// element is defined by RFC 1766 [RFC1766] which includes 
        /// a two-letter Language Code (taken from the ISO 639 standard [ISO639]), 
        /// followed, optionally, by a two-letter Country Code 
        /// (taken from the ISO 3166 standard [ISO3166]).
        /// For example, 'en' for English, 'fr' for French, or 'en-uk' 
        /// for English used in the United Kingdom.
        /// </para>
        /// </value>
        public string Language
        {
            get { return languageElement.InnerText; }
            set
            {
                languageElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Language");
            }
        }

        /// <summary>
        /// Gets the collection of pages in this score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="PathCollection"/>
        /// </para>
        /// <para>
        /// Each item is a pair in a form <i>PageName:PagePartUri</i> where the <i>PageName</i>
        /// is a user-defined name of the page (used just for an easier orientation) 
        /// and the <i>PagePartUri</i> is the Uri of the corresponding <see cref="PagePart"/>
        /// relative to the owner score's directory.
        /// <example>
        /// Path: For a page called <i>Preface</i> in the <i>/Scores/Bach/Organ/Art of Fugue</i> score
        /// the corresponding collection item could have the following form:<br/>
        /// <i>Preface:/Page1.xml</i>
        /// </example>
        /// The page name can contain spaces, the Uri cannot.
        /// </para>
        /// </value>
        public PathCollection Pages
        {
            get { return pages; }
        }

        /// <summary>
        /// Gets or sets the publisher of the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// An entity responsible for making the resource available.
        /// Examples of a Publisher include a person, an organization, 
        /// or a service. Typically, the name of a Publisher should be 
        /// used to indicate the entity.
        /// </para>
        /// <para>
        /// Follow standard AACR2 formatting if possible (Place followed by Publisher).<br/>
        /// New York : Cornell Masque Ithaca
        /// </para>
        /// </value>
        public string Publisher
        {
            get { return publisherElement.InnerText; }
            set
            {
                publisherElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Publisher");
            }
        }

        /// <summary>
        /// Gets or sets the rights over the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// Information about rights held in and over the resource.
        /// Typically, a Rights element will contain a rights management 
        /// statement for the resource, or reference a service providing 
        /// such information. Rights information often encompasses 
        /// Intellectual Property Rights (IPR), Copyright, and various Property Rights. 
        /// If the Rights element is absent, no assumptions can be made about the status 
        /// of these and other rights with respect to the resource.
        /// </para>
        /// </value>
        public string Rights
        {
            get { return rightsElement.InnerText; }
            set
            {
                rightsElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Rights");
            }
        }

        /// <summary>
        /// Gets or sets the subject of the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// The topic of the content of the resource.
        /// Typically, a Subject will be expressed as keywords, key phrases or classification codes 
        /// that describe a topic of the resource
        /// </para>
        /// <para>
        /// Recommended best practice is to select a value from a controlled vocabulary 
        /// or formal classification scheme. Subject headings, usually from controlled 
        /// vocabularies such as LCSH, LCTGM, AAT, etc. Also local vocabularies.
        /// </para>
        /// </value>
        public string Subject
        {
            get { return subjectElement.InnerText; }
            set
            {
                subjectElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Subject");
            }
        }

        /// <summary>
        /// Gets or sets the sub title of the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// The sub title may optionally extend the <see cref="Title"/>.
        /// </para>
        /// </value>
        public string SubTitle
        {
            get { return subTitleElement.InnerText; }
            set
            {
                subTitleElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("SubTitle");
            }
        }

        /// <summary>
        /// Gets or sets the title of the score.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// A name given to the resource. Typically, a Title will be a name by which the resource is formally known.
        /// </para>
        /// </value>
        public string Title
        {
            get { return titleElement.InnerText; }
            set
            {
                titleElement.InnerText = SecurityElement.Escape(value);

                RaisePropertyChanged("Title");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Date"/> property.
        /// </summary>
        private XmlElement dateElement;
        /// <summary>
        /// Holds the value of the <see cref="Description"/> property.
        /// </summary>
        private XmlElement descriptionElement;
        /// <summary>
        /// Holds the value of the <see cref="DefaultPageHeight"/> property.
        /// </summary>
        private XmlAttribute defaultPageHeightAttribute;
        /// <summary>
        /// Holds the value of the <see cref="DefaultPageWidth"/> property.
        /// </summary>
        private XmlAttribute defaultPageWidthAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Identifier"/> property.
        /// </summary>
        private XmlElement identifierElement;
        /// <summary>
        /// Holds the value of the <see cref="Language"/> property.
        /// </summary>
        private XmlElement languageElement;
        /// <summary>
        /// Holds the XML element that holds the metadata of the score.
        /// </summary>
        private XmlElement metadataElement;
        /// <summary>
        /// Holds the root OAI XML element.
        /// </summary>
        private XmlElement oaiElement;
        /// <summary>
        /// Holds the collection of pages in this score.
        /// </summary>
        private PathCollection pages;
        /// <summary>
        /// Holds a reference to the pages child element of the object element.
        /// </summary>
        private XmlElement pagesElement;
        /// <summary>
        /// Holds the value of the <see cref="Publisher"/> property.
        /// </summary>
        private XmlElement publisherElement;
        /// <summary>
        /// Holds the value of the <see cref="Rights"/> property.
        /// </summary>
        private XmlElement rightsElement;
        /// <summary>
        /// Holds the value of the <see cref="Subject"/> property.
        /// </summary>
        private XmlElement subjectElement;
        /// <summary>
        /// Holds the value of the <see cref="SubTitle"/> property.
        /// </summary>
        private XmlElement subTitleElement;
        /// <summary>
        /// Holds the value of the <see cref="Title"/> property.
        /// </summary>
        private XmlElement titleElement;

        #endregion
    }
}
